package com.tuniu.agents.common.handler;

import com.tuniu.agents.advisor.ChatEnvironmentMemory;
import com.tuniu.agents.common.command.CommandManagerUtil;
import com.tuniu.agents.common.command.action.DefaultCallAgentAction;
import com.tuniu.agents.common.command.action.DefaultOutputTextAction;
import com.tuniu.agents.common.command.match.StringMatch;
import com.tuniu.agents.common.entity.itinerary.BaseItinerary;
import com.tuniu.agents.common.entity.itinerary.Resource;
import com.tuniu.agents.common.entity.itinerary.enums.ResourceStatus;
import com.tuniu.agents.common.entity.itinerary.resource.HotelResource;
import com.tuniu.agents.common.i18n.I18nConstants;
import com.tuniu.agents.common.message.BaseAgentNewResp;
import com.tuniu.agents.common.message.CardTypeEnum;
import com.tuniu.agents.common.service.itinerary.ItineraryService;
import com.tuniu.agents.common.util.NumberingUtil;
import com.tuniu.agents.impl.hotel.HotelAgentConfig;
import com.tuniu.agents.memory.MemoryManager;
import com.tuniu.agents.message.MessageRender;
import com.tuniu.agents.tools.hotel.constant.HotelI18KeyConstants;
import com.tuniu.agents.tools.hotel.tool.HotelDetail;
import com.tuniu.agents.tools.hotel.tool.NlowestPriceHotelList;
import com.tuniu.agents.tools.hotel.tool.PlowestPriceHotelList;
import com.tuniu.agents.tools.hotel.utils.HotelCommonUtil;
import com.tuniu.agents.util.I18nUtil;
import com.tuniu.agents.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.groovy.util.Maps;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Default stream response handler implementation
 * Supports both recommendation and suggestion modes
 */
@Slf4j
public class HotelStreamResponseHandler implements IStreamResponseHandler {

    public static final String MESSAGE = "message";
    public static final String CONVERSATION_ID = "conversationId";
    private static final String HOTEL_ITEM_REGEX = "《([^》]+)》";
    private static final String HOTEL_SUGGEST_REGEX = "\\*\\*%s\\*\\*";

    private final String questionId;

    private String toolName;

    private final String agentId;

    private final MemoryManager memoryManager;

    // Matching patterns
    private final Pattern bracketPattern;

    private final Pattern angleBracketPattern = Pattern.compile("<([^>]+)>");

    private final ChatEnvironmentMemory chatEnvironmentMemory;

    private MessageRender messageRender;

    private ItineraryService itineraryService;

    /**
     * Constructor
     */
    public HotelStreamResponseHandler(String toolName, String agentId, String questionId,
                                      ChatEnvironmentMemory chatEnvironmentMemory, MemoryManager memoryManager, MessageRender messageRender,ItineraryService itineraryService) {
        this.bracketPattern = Pattern.compile(HOTEL_ITEM_REGEX);
        this.questionId = questionId;
        this.chatEnvironmentMemory = chatEnvironmentMemory;
        this.toolName = toolName;
        this.agentId = agentId;
        this.memoryManager = memoryManager;
        this.messageRender = messageRender;
        this.itineraryService = itineraryService;
    }

    /**
     * Constructor
     */
    public HotelStreamResponseHandler(String toolName, String agentId, ChatEnvironmentMemory chatEnvironmentMemory, MemoryManager memoryManager, MessageRender messageRender,ItineraryService itineraryService) {
        this(toolName, agentId, "", chatEnvironmentMemory, memoryManager, messageRender,itineraryService);
    }


    @Override
    public Flux<String> handle(Map<String, Object> handlerContext, Flux<String> flux) {
        AtomicBoolean inSuggestionMode = new AtomicBoolean(false);
        String message = (String) handlerContext.get(MESSAGE);
        String conversationId = (String) handlerContext.get(CONVERSATION_ID);
        if (checkInValidDate(conversationId)) {
            return Flux.just(BaseAgentNewResp.sendAnswerMsg(null,I18nUtil.getMessage(conversationId, HotelI18KeyConstants.HOTEL_LIST_DATE_ERROR_MESSAGE)));
        }
        Map<String, Object> toolResponseMap = getToolResponseMap(toolName, message);
        if (toolResponseMap == null || toolResponseMap.isEmpty()) {
            return Flux.just(BaseAgentNewResp.sendAnswerMsg(null,I18nUtil.getMessage(conversationId, HotelI18KeyConstants.HOTEL_LIST_ERROR_MESSAGE)));
        }
        String cardType = getCardTypeByToolName(toolName);
        // Create a buffer for accumulating processing stream
        StringBuilder buffer = new StringBuilder();
        StringBuilder suggestBuffer = new StringBuilder();
        // Store recommendation information in markdown format
        StringBuilder recommendHotelMarkdown = new StringBuilder();

        AtomicBoolean hasCardInfo = new AtomicBoolean(false);
        Set<String> recommendHotels = new HashSet<>();
        AtomicInteger index = new AtomicInteger(1);
        return flux.flatMap(chunk -> {
                    if (!StringUtils.hasText(chunk)) {
                        return Flux.empty();
                    }

                    // If already in suggestion mode, accumulate but don't output
                    if (inSuggestionMode.get()) {
                        suggestBuffer.append(chunk);
                        return Flux.empty();
                    }

                    // Accumulate content
                    buffer.append(chunk);
                    String content = buffer.toString();

                    // First try to match bracket pattern
                    Matcher bracketMatcher = bracketPattern.matcher(content);
                    if (bracketMatcher.find()) {
                        hasCardInfo.set(true);
                        return processMatcherResult(bracketMatcher, buffer, toolResponseMap, cardType, message, recommendHotels, recommendHotelMarkdown, conversationId, index);
                    }

                    // If bracket pattern not matched, try angle bracket pattern
                    Matcher angleMatcher = angleBracketPattern.matcher(content);
                    if (angleMatcher.find()) {
                        hasCardInfo.set(true);
                        return processMatcherResult(angleMatcher, buffer, toolResponseMap, cardType, message, recommendHotels, recommendHotelMarkdown, conversationId, index);
                    }

                    // Process suggestion matching
                    Pattern suggestionPattern= Pattern.compile(String.format(HOTEL_SUGGEST_REGEX, I18nUtil.getMessage(conversationId, "hotel.prompt.recommend.example.suggest")));
                    Matcher suggestionMatcher = suggestionPattern.matcher(content);
                    if (suggestionMatcher.find()) {
                        inSuggestionMode.set(true);
                        suggestBuffer.append(content.substring(suggestionMatcher.start()));
                        buffer.setLength(0); // Clear buffer
                    }

                    return Flux.empty();
                })
                // Add the final suggestion content
                .concatWith(Mono.fromCallable(() -> {
                    // Output only suggestions if card info exists, otherwise return remaining content
                    if (hasCardInfo.get()) {
                        String suggest = suggestBuffer.toString();
                        if (!recommendHotelMarkdown.isEmpty()) {
                            if (StringUtils.hasText(suggest)){
                                // Add suggestion and summary content
                                recommendHotelMarkdown.append(suggest.trim());
                            }
                            memoryManager.setMessageRight(conversationId,
                                    Collections.singletonList(new AssistantMessage(recommendHotelMarkdown.toString(), Map.of("agentId", agentId))));
                        }
                        if (StringUtils.hasText(suggest)) {
                            return createSuggestionResponse(suggest);
                        }
                    } else {
                        return buffer.toString();
                    }
                    return "";  // Return empty string instead of null
                }).filter(StringUtils::hasText));  // Filter out empty strings
    }

    /**
     * Check if date is invalid
     * 
     * @param conversationId Conversation ID
     * @return True if date is expired, false otherwise
     */
    private Boolean checkInValidDate(String conversationId) {
        Object object = chatEnvironmentMemory.get(conversationId).get(conversationId + "_request");
        if (object instanceof NlowestPriceHotelList.Request poiRequest) {
            return HotelCommonUtil.checkDateExpire(poiRequest.checkIn());
        }
        if (object instanceof PlowestPriceHotelList.Request pkgRequest) {
            return HotelCommonUtil.checkDateExpire(pkgRequest.checkIn());
        }
        return Boolean.FALSE;
    }

    /**
     * Process matcher results
     * 
     * @param matcher Matcher for hotel name patterns
     * @param buffer Content buffer
     * @param toolResponseMap Response data map
     * @param cardType Card type for rendering
     * @param message Original message
     * @param recommendHotels Set of recommended hotels
     * @param recommendHotelMarkdown Markdown builder for recommendations
     * @param conversationId Conversation ID
     * @param index Sequential index for recommendations
     * @return Flux of processed responses
     */
    private Flux<String> processMatcherResult(Matcher matcher, StringBuilder buffer, Map<String, Object> toolResponseMap,
                                              String cardType, String message, Set<String> recommendHotels, StringBuilder recommendHotelMarkdown, String conversationId, AtomicInteger index) {
        String key = extractKey(matcher);
        int endIndex = matcher.start();
        String content = buffer.toString();
        String reason = extractReason(content, endIndex);

        // Update buffer, remove the processed part
        buffer.delete(0, matcher.end());

        if (toolResponseMap == null) {
            log.error("HotelStreamResponseHandler toolResponseMap is null, message:{}", message);
            return Flux.just(BaseAgentNewResp.sendErrorMsg("HotelStreamResponseHandler toolResponseMap is null", questionId));
        }
        // Skip if already recommended
        if (isRecommend(key, recommendHotels)) {
            return Flux.just("");
        }
        Object data = toolResponseMap.get(key);
        if (data == null) {
            log.error("HotelStreamResponseHandler toolResponseMap.get(key) is null, key:{}, toolResponseMap:{}", key, JsonUtil.toJson(toolResponseMap));
            return Flux.just(BaseAgentNewResp.sendErrorMsg("HotelStreamResponseHandler toolResponseMap.get(key) is null, key:" + key, questionId));
        }
        String saveInMdReason = StringUtils.hasText(reason) && reason.contains("：") ? reason.substring(reason.indexOf("：") + 1) : reason;

        Map<String, Object> hotelDataMap = JsonUtil.toMap(JsonUtil.toJson(data));
        hotelDataMap.put("recommendationReason",saveInMdReason);
        // Markdown message
        String markdownResponse = null;
        String viewMarkdownResponse = null;
        String acceptLanguage = I18nUtil.getLocaleLanguageTag(conversationId);
        if (CardTypeEnum.HOTEL_LIST_ONE.getCardType().equals(cardType)) {
            long indexNumber = NumberingUtil.incrWithExpire(conversationId);
            markdownResponse = messageRender.renderTemplate("hotel/hotel-list-one.ftlh",Map.of(I18nConstants.LANGUAGE_TAG, acceptLanguage), hotelDataMap);
            // Save Command
            viewMarkdownResponse = NumberingUtil.getNumberMarkdown(markdownResponse, "#### ", indexNumber);
            CommandManagerUtil.saveListCommand(conversationId, cardType + "+" + indexNumber, "stringMatch" , "defaultCallAgentAction",
                    Maps.of(DefaultCallAgentAction.PARAM_AGENT_ID, HotelAgentConfig.HOTEL_TOOLS, DefaultOutputTextAction.KEY_MESSAGE, key, StringMatch.KEY_STRING, List.of(String.valueOf(indexNumber), key)));
        } else if (CardTypeEnum.HOTEL_PACKAGE_LIST_ONE.getCardType().equals(cardType)){
            long indexNumber = NumberingUtil.incrWithExpire(conversationId);
            markdownResponse = messageRender.renderTemplate("hotel/hotel-package-list-one.ftlh", Map.of(I18nConstants.LANGUAGE_TAG, acceptLanguage), hotelDataMap);
            // Save Command
            viewMarkdownResponse = NumberingUtil.getNumberMarkdown(markdownResponse, "#### ", indexNumber);
            CommandManagerUtil.saveListCommand(conversationId, cardType + "+" + indexNumber, "stringMatch" , "defaultCallAgentAction",
                    Maps.of(DefaultCallAgentAction.PARAM_AGENT_ID, HotelAgentConfig.HOTEL_TOOLS, DefaultOutputTextAction.KEY_MESSAGE, key, StringMatch.KEY_STRING, List.of(String.valueOf(indexNumber), key)));
        }

        if (recommendHotels.size() == 1 ) {
            BaseItinerary hotelItinerary = itineraryService.getItinerary(conversationId, null);
            if(hotelItinerary != null) {
                Resource hotelResource = null;
                if( data instanceof NlowestPriceHotelList.HotelDataAbbr hotelDataAbbr) {
                    hotelResource = new HotelResource(hotelDataAbbr.chineseName(), hotelDataAbbr.checkIn(), hotelDataAbbr.checkOut(), JsonUtil.toJson(data), markdownResponse, new BigDecimal(hotelDataAbbr.lowestPrice()), ResourceStatus.PENDING);
                }else if(data instanceof PlowestPriceHotelList.PackageRow packageRow){
                    hotelResource = new HotelResource(packageRow.hotelName(),packageRow.checkIn(),packageRow.checkOut(),JsonUtil.toJson(data),markdownResponse,new BigDecimal(packageRow.packagePrice()),ResourceStatus.PENDING);
                }
                if(hotelResource != null) {
                    itineraryService.saveResource(conversationId, null, hotelResource);
                }
            }
        }

        buildRecommendHotelMarkdown(saveInMdReason, data, recommendHotelMarkdown);
        String response = createItemResponse(data, viewMarkdownResponse, cardType);
        return Flux.just(response);
    }

    /**
     * Build recommendation hotel markdown based on the data type
     * 
     * @param reason Recommendation reason
     * @param data Hotel data object
     * @param recommendHotelMarkdown Markdown builder
     */
    private void buildRecommendHotelMarkdown(String reason, Object data, StringBuilder recommendHotelMarkdown) {
        // Record markdown information to be stored
        if (data instanceof NlowestPriceHotelList.HotelDataAbbr hotelDataAbbr) {
            buildPoiHotelMarkdown(reason, hotelDataAbbr, recommendHotelMarkdown);
        }
        if (data instanceof PlowestPriceHotelList.PackageRow packageRow) {
            buildPkgHotelMarkdwon(reason, packageRow, recommendHotelMarkdown);
        }
    }

    /**
     * Build POI hotel markdown for display
     * 
     * @param reason Recommendation reason
     * @param hotelData Hotel data
     * @param recommendHotelMarkdown Markdown builder
     */
    private void buildPoiHotelMarkdown(String reason, NlowestPriceHotelList.HotelDataAbbr hotelData, StringBuilder recommendHotelMarkdown) {
        if (recommendHotelMarkdown.isEmpty()) {
            recommendHotelMarkdown.append("|Hotel Name|Room Type|Check-in Date|Check-out Date|Lowest Price|Meals|Cancellation Policy|Review Summary|Business District|Distance|Recommendation Reason|\n");
            recommendHotelMarkdown.append("|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|\n");
        }
        recommendHotelMarkdown.append("|").append(hotelData.chineseName())
                .append("|").append(hotelData.roomName())
                .append("|").append(hotelData.checkIn())
                .append("|").append(hotelData.checkOut())
                .append("|").append(hotelData.lowestPrice())
                .append("|").append(hotelData.lowestPrice())
                .append("|").append(hotelData.lowestPrice())
                .append("|").append(hotelData.refund())
                .append("|").append(hotelData.commentDigest())
                .append("|").append(hotelData.business())
                .append("|").append(reason)
                .append("|\n");
    }

    /**
     * Build package hotel markdown for display
     * 
     * @param reason Recommendation reason
     * @param packageRow Package data
     * @param recommendHotelMarkdown Markdown builder
     */
    private void buildPkgHotelMarkdwon(String reason, PlowestPriceHotelList.PackageRow packageRow, StringBuilder recommendHotelMarkdown) {
        if (recommendHotelMarkdown.isEmpty()) {
            recommendHotelMarkdown.append("|Hotel Name|Room Type|Package Name|Check-in Date|Check-out Date|Price|Star Rating|Review Score|Recommendation Reason\n");
            recommendHotelMarkdown.append("|--------|--------|--------|--------|--------|--------|--------|--------|--------\n");
        }
        recommendHotelMarkdown.append("|").append(packageRow.hotelName())
                .append("|").append(packageRow.roomName())
                .append("|").append(packageRow.packageName())
                .append("|").append(packageRow.checkIn())
                .append("|").append(packageRow.checkOut())
                .append("|").append(packageRow.packagePrice())
                .append("|").append(packageRow.starName())
                .append("|").append(packageRow.commentScore())
                .append("|").append(reason)
                .append("|\n");
    }

    /**
     * Check if a hotel has already been recommended
     * 
     * @param hotelName Hotel name
     * @param recommendHotels Set of recommended hotels
     * @return True if already recommended, false otherwise
     */
    private Boolean isRecommend(String hotelName, Set<String> recommendHotels) {
        if (recommendHotels.contains(hotelName)) {
            return true;
        }
        recommendHotels.add(hotelName);
        return false;
    }

    /**
     * Extract key from matcher
     * 
     * @param matcher Pattern matcher
     * @return Extracted key
     */
    private String extractKey(Matcher matcher) {
        return matcher.group(1).trim();
    }

    /**
     * Extract reason from content
     * 
     * @param content Content string
     * @param endIndex End index of match
     * @return Extracted reason
     */
    private String extractReason(String content, int endIndex) {
        return content.substring(0, endIndex).trim();
    }

    /**
     * Create item response
     * 
     * @param data Data object
     * @param reason Reason text
     * @param cardType Card type
     * @return Response string
     */
    private String createItemResponse(Object data, String reason, String cardType) {
        return BaseAgentNewResp.sendCardMsg(
                cardType,
                Collections.singletonList(data),
                reason,
                questionId
        );
    }

    /**
     * Create suggestion response
     * 
     * @param suggestion Suggestion text
     * @return Response string
     */
    private String createSuggestionResponse(String suggestion) {
        return BaseAgentNewResp.sendAnswerMsg(questionId, suggestion);
    }

    /**
     * Get tool response map based on tool name
     * 
     * @param toolName Tool name
     * @param toolResponse Tool response string
     * @return Map of tool response data
     */
    private Map<String, Object> getToolResponseMap(String toolName, String toolResponse) {
        Map<String, Object> toolResponseMap = null;
        switch (toolName) {
            case NlowestPriceHotelList.TOOLNAME:
                toolResponseMap = JsonUtil.fromJson(toolResponse, NlowestPriceHotelList.Response.class).data()
                        .stream()
                        .map(NlowestPriceHotelList::buildNewData)
                        .collect(Collectors.toMap(NlowestPriceHotelList.HotelDataAbbr::chineseName, Function.identity(), (a, b) -> a));
                break;
            case PlowestPriceHotelList.TOOLNAME:
                toolResponseMap = JsonUtil.fromJson(toolResponse, PlowestPriceHotelList.Response.class).data().rows()
                        .stream()
                        .sorted(Comparator.comparing(hotel -> Optional.ofNullable(hotel.packagePrice()).orElse(0.0), Double::compare))
                        .map(PlowestPriceHotelList::buildNewData)
                        .collect(Collectors.toMap(PlowestPriceHotelList.PackageRow::hotelName, Function.identity(), (a, b) -> a));
        }
        return toolResponseMap;
    }

    /**
     * Get card type based on tool name
     * 
     * @param toolName Tool name
     * @return Card type string
     */
    private String getCardTypeByToolName(String toolName) {
        return switch (toolName) {
            case NlowestPriceHotelList.TOOLNAME -> CardTypeEnum.HOTEL_LIST_ONE.getCardType();
            case PlowestPriceHotelList.TOOLNAME -> CardTypeEnum.HOTEL_PACKAGE_LIST_ONE.getCardType();
            case HotelDetail.TOOLNAME -> CardTypeEnum.HOTEL_DETAIL.getCardType();
            default -> CardTypeEnum.HOTEL_LIST_ONE.getCardType();
        };
    }
}